#include "GeneralTextWidget.h"
#include "GeneralTextViewAdaptor.h"
#include "widget/CodeEdit.h"

#include "simodo/lsp-client/LspEnums.h"
#include "simodo/shell/access/LspAccess_interface.h"

#include <QComboBox>
#include <QVBoxLayout>
#include <QAction>
#include <QToolTip>
#include <QAbstractItemView>

using namespace simodo;

GeneralTextWidget::GeneralTextWidget(GeneralTextViewAdaptor & view, QTextDocument * document)
    : _view(view)
{
    _edit = new CodeEdit(_view.params());
    _edit->installEventFilter(this);
    _edit->setDocument(document);

    _combo = new QComboBox();
    _combo->setVisible(false);
    _combo->setFocusPolicy(Qt::NoFocus);
    _combo->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);

    QVBoxLayout * layout = new QVBoxLayout(this);
    layout->addWidget(_combo);
    layout->addWidget(_edit);
    layout->setMargin(0);
    layout->setSpacing(0);
    layout->setContentsMargins(0,0,0,0);

#ifndef QT_NO_CLIPBOARD
    connect(_edit, &QPlainTextEdit::copyAvailable, shell().cut_action(), &QAction::setEnabled);
    connect(_edit, &QPlainTextEdit::copyAvailable, shell().copy_action(), &QAction::setEnabled);
#endif

    if (_edit->params().lsp_support)
        connect(_edit, &CodeEdit::requestDeclaration, this,
            [this](int line, int character){
                shell::LspAccess_interface * lsp = shell().getLspAccess(_view.path_to_file());
                if (lsp)
                    lsp->declaration(_view.path_to_file(), line, character);
            });

    if (_edit->params().lsp_support)
        connect(_edit, &CodeEdit::requestDefinition, this,
            [this](int line, int character){
                shell::LspAccess_interface * lsp = shell().getLspAccess(_view.path_to_file());
                if (lsp)
                    lsp->definition(_view.path_to_file(), line, character);
            });

    connect(&_view, &GeneralTextViewAdaptor::gotZoom, this, 
            [this](int size){
                if (size >= 4 && size < 31) {
                    QFont f = _edit->font();
                    f.setPointSize(size);
                    _edit->resetFont(f);
                }
            });

    if (_edit->params().lsp_support) 
        connect(&_view, &GeneralTextViewAdaptor::gotPublishDiagnostics, this, 
            [this](){
                _edit->rehighlight();

                if (!_edit->params().lsp_completer)
                    return;

                shell::LspAccess_interface * lsp = shell().getLspAccess(_view.path_to_file());
                if (!_edit->completer() || !lsp)
                    return;

                if (_last_line == -1) {
                    QTextCursor cursor = _edit->textCursor();
                    lsp->completion(_edit,
                                    _view.path_to_file(), cursor.blockNumber(), cursor.positionInBlock(),
                                    simodo::lsp::CompletionTriggerKind::Invoked, "");
                }
                else if (!_need_completion_trigger.isEmpty()) {
                    QTextCursor cursor = _edit->textCursor();
                    if (_edit->trigger_characters().contains(_need_completion_trigger)) {
                        lsp->completion(_edit,
                                        _view.path_to_file(), cursor.blockNumber(), cursor.positionInBlock(),
                                        simodo::lsp::CompletionTriggerKind::TriggerCharacter, _need_completion_trigger);
                        // shell().sendGlobal("Completion request #3 for '" + _need_completion_trigger + "'");
                    }
                    else {
                        lsp->completion(_edit,
                                        _view.path_to_file(), cursor.blockNumber(), cursor.positionInBlock(),
                                        simodo::lsp::CompletionTriggerKind::Invoked, "");
                        // shell().sendGlobal("Completion request #4 for '" + _need_completion_trigger + "'");
                    }
                }
            });

    if (_edit->params().document_symbols)
        connect(&_view, &GeneralTextViewAdaptor::gotDocumentSymbol, this, 
            [this](){
                const QVector<shell::DocumentSymbol> & symbols = _view.symbols();
                if (symbols.empty() || !_edit->params().document_symbols)
                    return;

                // double edit_font_size   = QFontMetricsF(_combo->font()).horizontalAdvance(' ');
                // double width            = _combo->width();
                // int    chars_count      = qRound(width/edit_font_size)-1;

                const QIcon item_icon_variable = QIcon::fromTheme("code-variable", QIcon(":/images/code-variable"));
                const QIcon item_icon_function = QIcon::fromTheme("code-function", QIcon(":/images/code-function"));

                _combo->clear();
                _combo->addItem("");
                for(const shell::DocumentSymbol & ds : symbols) {
                    QString text;
                    if (!ds.detail.isEmpty())
                        text = ds.detail;
                    else 
                        text = QString::fromStdU16String(lsp::getSymbolKindString(ds.kind)) + " " + ds.name;

                    // if (text.size() > chars_count) {
                    //     text.truncate(chars_count-3);
                    //     text += "...";
                    // }

                    if (ds.kind == lsp::SymbolKind::Function || ds.kind == lsp::SymbolKind::Method)
                        _combo->addItem(item_icon_function, text);
                    else
                        _combo->addItem(item_icon_variable, text);
                }
                _combo->setVisible(true);

                QTextCursor cursor = code_edit()->textCursor();
                checkDocSymbolChange(cursor.blockNumber(), cursor.columnNumber());
            });

    if (_edit->params().semantic_tokens)
        connect(&_view, &GeneralTextViewAdaptor::gotSemanticTokens, this, 
            [this]{
                _edit->rehighlight();
            });

    if (_edit->params().lsp_hover)
        connect(&_view, &GeneralTextViewAdaptor::gotHover, this, 
            [this](const void * sender, const QString & , const QString & value){
                if (sender == _edit)
                    _edit->hover(value);
            });

    if (_edit->params().lsp_completer)
        connect(&_view, &GeneralTextViewAdaptor::gotCompletionItems, this, 
            [this](const void * sender){
                if (sender != _edit)
                    return;

                QCompleter * c = _edit->completer();
                if (c) {
                    uint64_t sum = _view.completions().size();
                    for(const shell::CompletionItem & i : _view.completions()) {
                        std::u16string s = i.label.toStdU16String();
                        for(size_t j=0; j < s.length(); ++j)
                            sum += static_cast<uint64_t>(s[j]);
                    }
                    if (sum == _control_sum) {
                        // shell().sendGlobal("Completion cashed", shell::MessageSeverity::Debug);
                        return;
                    }
                    _control_sum = sum;

                    _completer_model.clear();

                    const QIcon item_icon_variable = QIcon::fromTheme("code-variable", QIcon(":/images/code-variable"));
                    const QIcon item_icon_function = QIcon::fromTheme("code-function", QIcon(":/images/code-function"));

                    for(const shell::CompletionItem & i : _view.completions()) {
                        _completer_model.appendRow({
                            new QStandardItem(i.labelDetails.detail.contains("(") 
                                                                ? item_icon_function : item_icon_variable, 
                                                                i.label),
                            new QStandardItem(i.labelDetails.detail),
                            new QStandardItem(i.labelDetails.description)
                        });
                    }

                    if (_edit->params().use_lexis_for_completer)
                        for(const QString & s : _view.lexis_symbols()) {
                            _completer_model.appendRow({
                                new QStandardItem(item_icon_variable, s),
                                new QStandardItem("lexical"),
                                new QStandardItem("")
                            });
                        }

                    _completer_model.sort(0);
                }
            });

    connect(symbols_combo(), QOverload<int>::of(&QComboBox::activated), this, 
            [this](int index){
                const QVector<shell::DocumentSymbol> & symbols = _view.symbols();

                if (index <= 0 || index > symbols.size())
                    return;
                index--;
                const shell::DocumentSymbol & ds = symbols[index];
                _edit->setLineCol({int(ds.selectionRange.start().line()+1), int(ds.selectionRange.start().character()+1)});
            });

    connect(_edit, &QPlainTextEdit::cursorPositionChanged, this, 
            [this]()
            {
                QTextCursor cursor = code_edit()->textCursor();
                shell().displayCursorPosition(tr("pos %1, line %2, col %3")
                                                    .arg(cursor.position())
                                                    .arg(cursor.blockNumber()+1)
                                                    .arg(cursor.columnNumber()+1));

                shell::LspAccess_interface * lsp = shell().getLspAccess(_view.path_to_file());

                if (_edit->completer() && lsp
                 && (_last_line != cursor.blockNumber() || _last_character != cursor.positionInBlock())) {
                    if (_last_line == -1 && cursor.blockNumber() == 0) 
                        _last_line = 0, _last_character = 0;
                    else if (_last_line != cursor.blockNumber()) {
                        _last_line      = cursor.blockNumber();
                        _last_character = -1;
                        _last_left_punctuation = _edit->selectLeftPunctuation().selectedText();
                        _need_completion_trigger = "";

                        if (_cursor_position_change_timer > 0)
                            killTimer(_cursor_position_change_timer);
                        _cursor_position_change_timer = startTimer(
                                                        _edit->params().cursor_position_change_delay_mills);
                    }
                    else {
                        QString text      = cursor.block().text();
                        int     character = cursor.positionInBlock();
                        int     start     = std::max(0,std::min(character, _last_character));
                        int     end       = std::max(character, _last_character);

                        bool    need_force_update = false;

                        for(int i=start; i < end && i < text.length(); ++i)
                            if (not_a_word.contains(text[i])) {
                                need_force_update = true;
                                break;
                            }

                        _last_character = character;

                        QString left_punctuation  = _edit->selectLeftPunctuation().selectedText();

                        if (left_punctuation != _last_left_punctuation || need_force_update) {
                            _last_left_punctuation = left_punctuation;

                            if (_cursor_position_change_timer > 0)
                                killTimer(_cursor_position_change_timer);

                            _cursor_position_change_timer = startTimer(
                                                            _edit->params().cursor_position_change_delay_mills);
                        }
                    }
                }

                checkDocSymbolChange(cursor.blockNumber(), cursor.columnNumber());
            });

    if (_edit->params().lsp_hover)
        connect(_edit, &CodeEdit::requestHover, this,
            [this](int line, int character){
                shell::LspAccess_interface * lsp = shell().getLspAccess(_view.path_to_file());
                if (lsp)
                    lsp->hover(_edit, _view.path_to_file(), line, character);
            });

    if (_edit->params().lsp_completer)
        connect(_edit, &CodeEdit::requestForceCompletion, this,
            [this]{
                _view.forceCompletion();
            });

    connect(_edit, &CodeEdit::gotFocus, this,
            [this]{
                connect(_edit->document(), &QTextDocument::undoAvailable, 
                        shell().edit_undo_action(), &QAction::setEnabled);
                connect(_edit->document(), &QTextDocument::redoAvailable, 
                        shell().edit_redo_action(), &QAction::setEnabled);
                connect(shell().edit_undo_action(), &QAction::triggered, 
                        _edit, &QPlainTextEdit::undo);
                connect(shell().edit_redo_action(), &QAction::triggered, 
                        _edit, &QPlainTextEdit::redo);

                QTextCursor cursor = _edit->textCursor();
                shell().displayCursorPosition(tr("pos %1, line %2, col %3")
                                                    .arg(cursor.position())
                                                    .arg(cursor.blockNumber()+1)
                                                    .arg(cursor.columnNumber()+1));
                int scaling_percent = qRound(static_cast<double>(_edit->font().pointSize()) / _edit->params().font_zero_size * 100.0);
                _view.shell().displayStatistics(QString::number(scaling_percent)+"%");
                _view.setCurrentEditor(_edit);
            });
    connect(_edit, &CodeEdit::lostFocus, this,
            [this]{
                disconnect(_edit->document(), &QTextDocument::undoAvailable, 
                            shell().edit_undo_action(), &QAction::setEnabled);
                disconnect(_edit->document(), &QTextDocument::redoAvailable, 
                            shell().edit_redo_action(), &QAction::setEnabled);
                disconnect(shell().edit_undo_action(), &QAction::triggered, 
                            _edit, &QPlainTextEdit::undo);
                disconnect(shell().edit_redo_action(), &QAction::triggered, 
                            _edit, &QPlainTextEdit::redo);
                _view.setCurrentEditor(nullptr);
            });

    connect(_edit, &CodeEdit::requestScreenshot, this, 
            [this]{
                if (_edit->getScreenshot().save(_view.path_to_file() + ".png", "PNG"))
                    _view.shell().openFile(_view.path_to_file() + ".png");
            });

    connect(_edit, &CodeEdit::requestSaveAsPicture, this, 
            [this]{
                if (_edit->toPixmap().save(_view.path_to_file() + ".png", "PNG"))
                    _view.shell().openFile(_view.path_to_file() + ".png");
            });
}

shell::Access_interface & GeneralTextWidget::shell() 
{ 
    return _view.shell(); 
}

void GeneralTextWidget::readyToWork()
{
    QFontDatabase database;
    QFont         font;

    if (!_edit->params().font_name.isEmpty()
    /// \note При задании конкретного шрифта, не ограничиваем
    //  && database.isFixedPitch(_edit->params().font_name)
    )
        font.setFamily(_edit->params().font_name);
    else {
        bool matched = false;

        for (const QString & family : _edit->params().font_names) 
            if (database.isFixedPitch(family)) {
                matched = true;
                font.setFamily(family);
                _view.shell().sendGlobal(tr("Font '%1' is selected").arg(font.family()));
                break;
            }

        if (!matched)
            for (const QString & family : database.families())
                if (family != "Monospace" && database.isFixedPitch(family)) {
                    matched = true;
                    font.setFamily(family);
                    _view.shell().sendGlobal(tr("Font '%1' is selected").arg(font.family()),
                                            shell::MessageSeverity::Warning);
                    break;
                }

        if (!matched) {
            font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
            _view.shell().sendGlobal(tr("Font '%1' is selected").arg(font.family()),
                                        shell::MessageSeverity::Warning);
        }

        font.setFixedPitch(true);
    }

    font.setPointSize(_view.params().font_default_size);
    _edit->setFont(font);
    _edit->setLineWrapMode(_edit->params().wrap_lines
                                ? QPlainTextEdit::LineWrapMode::WidgetWidth
                                : QPlainTextEdit::LineWrapMode::NoWrap);

    font.setPointSize(_view.params().font_zero_size);
    symbols_combo()->setFont(font);

    shell::LspAccess_interface * lsp = shell().getLspAccess(_view.path_to_file());
    if (lsp && _edit->params().lsp_completer) {
        _edit->resetCompleter(&_completer_model, 
                    lsp->getServerCapabilities(_view.path_to_file()).completionProvider.triggerCharacters);

        QCompleter * c = _edit->completer();
        if (c) {
            c->popup()->setMouseTracking(true);
            QFont f = _edit->font();
            QToolTip::setFont(f);
            connect(c->popup(), &QAbstractItemView::entered, this, [c](const QModelIndex & index){
                const QAbstractItemModel * model = index.model();
                if (model) {
                    QString detail = model->data(model->index(index.row(),1)).toString();
                    QString description = model->data(model->index(index.row(),2)).toString();
                    QString text = "<p>" + detail + "</p>";
                    if (!description.isEmpty())
                        text += "<p>" + description + "</p>";
                    QPoint p = c->popup()->pos();
                    p.setX(p.x() + c->popup()->width());
                    QToolTip::showText(p, text);
                }
            });
        }
    }

    _edit->resetMinimap();
}

void GeneralTextWidget::nearToClose()
{
    _edit->nearToClose();
}

void GeneralTextWidget::timerEvent(QTimerEvent * event)
{
    if (event->timerId() == _cursor_position_change_timer) {
        killTimer(_cursor_position_change_timer);
        _cursor_position_change_timer = 0;

        QTextCursor cursor = _edit->textCursor();

        shell::LspAccess_interface * lsp = shell().getLspAccess(_view.path_to_file());
        if (lsp && _edit->params().lsp_completer) {
            QTextCursor cursor           = _edit->textCursor();
            QString     left_punctuation = _edit->selectLeftPunctuation().selectedText();
            if (_edit->trigger_characters().contains(left_punctuation)) {
                lsp->completion(_edit,
                                _view.path_to_file(), cursor.blockNumber(), cursor.positionInBlock(),
                                simodo::lsp::CompletionTriggerKind::TriggerCharacter, left_punctuation);
                // shell().sendGlobal("Completion request #1 for '" + left_punctuation + "'");
            }
            else {
                lsp->completion(_edit,
                                _view.path_to_file(), cursor.blockNumber(), cursor.positionInBlock(),
                                simodo::lsp::CompletionTriggerKind::Invoked, "");
                // shell().sendGlobal("Completion request #2 for '" + left_punctuation + "'");
            }
        }
    }
}

void GeneralTextWidget::focusInEvent(QFocusEvent * event)
{
    QWidget::focusInEvent(event);

    if(event->gotFocus())
        _edit->setFocus();
}

bool GeneralTextWidget::eventFilter(QObject * object, QEvent *event)
{
    if (event->type() == QEvent::KeyPress) {
        QKeyEvent * key_event = static_cast<QKeyEvent *>(event);
        if (key_event->key() == Qt::Key_Equal && key_event->modifiers().testFlag(Qt::ControlModifier)) {
            _view.zoomIn();
            return true;
        }
    }

    return QWidget::eventFilter(object, event);
}

void GeneralTextWidget::checkDocSymbolChange(int line, int character)
{
    if (!_combo->isVisible())
        return;

    /// Для того чтобы корректно обработать случаи накладывания одного промежутка символа на другой,
    /// придётся проверить все символы, выбрав из подходящих (тех, для которых checkPositionInsideRange == True)
    /// последний по началу (range.start).
    /// Тут полагаю, что случай накладывания может быть только такой, что один промежуток <b>строго внутри</b> другого!

    bool isFound = false;
    int foundIndex = 0;
    lsp::Range foundRange;

    for (int i = 0; i < _view.symbols().size(); ++i) {
        const shell::DocumentSymbol & ds = _view.symbols()[i];
        inout::Position pos(line, character);
        if (ds.range.contains(pos)) {
            if (not isFound or foundRange.start() < ds.range.start()) {
                foundIndex = i;
                foundRange = ds.range;
            }
            isFound = true;
        }
    }

    if (not isFound) {
        _combo->setCurrentIndex(0);
        return;
    }

    if (_combo->currentIndex() != foundIndex + 1)
        _combo->setCurrentIndex(foundIndex + 1);
}

